using System;
using System.IO;
using Mono.Cecil;

namespace StubGenerator
{
	public class StubGenerator
	{
		private const string EXCEPTION_IMPLEMENTATION = "{  throw new NotImplementedException(); }\n";
		private const string IDONTIMPLEMENT = "__IDontImplement__";
		private readonly ModuleDefinition _module;
		readonly string _basedir;
		readonly string[] _dontmodify;
		readonly string[] _usingNamespaces;
		public string [] perMethodAttributes;
		
		public StubGenerator(string assemblyPath, string outputdir, string [] dontmodify, string [] usingNamespaces)
		{
			AssemblyDefinition assemDef = AssemblyFactory.GetAssembly (assemblyPath);
			if (outputdir.EndsWith("\\")) _basedir = outputdir; else _basedir = outputdir + '\\';
			_module = assemDef.MainModule;
			_dontmodify = dontmodify;
			_usingNamespaces = usingNamespaces;
		}

		public void doWork() {
			ConvertTypes ();
		}

		//TODO: no handling of generics !!
		private static string getTypeName(TypeReference typeDef, bool isout)
		{
			string paramtype = typeDef.FullName;
			if (paramtype == "System.Void") return "void";
			if (paramtype == "System.Int32") return "int";
			if (paramtype == "System.CodeDom.Compiler.CodeDomProvider")
				return "object";
			if (paramtype == "Accessibility.IAccessible")
				return IDONTIMPLEMENT;
			if (paramtype.EndsWith("&"))
				paramtype = (isout ? "out " : "ref ") + paramtype.TrimEnd("&".ToCharArray());
			paramtype = paramtype.Replace('/', '.');
			paramtype = paramtype.Replace("`1<", "<");
			paramtype = paramtype.Replace ("`2<", "<");
			paramtype = paramtype.Replace ("`3<", "<");
			return paramtype;
		}
		
		private static string getTypeName(TypeReference typeDef) { return getTypeName(typeDef, false); }

		private void writePerMethodAttributes (TextWriter os, int indentation) 
		{
			if (perMethodAttributes != null)
				foreach (string attr in perMethodAttributes)
					WriteToStream (os, "[" + attr + "]\n", indentation);
		}

		private void outputMethodName (MethodDefinition methodDef, TextWriter os) 
		{
			string methodName = methodDef.Name;
			if (methodDef.IsConstructor) {
				methodName = methodDef.DeclaringType.Name;
				if (methodName.IndexOf("`") != -1)
					methodName = methodName.Remove (methodName.IndexOf ("`"));
			}
			os.Write (methodName);
			
			if (methodDef.GenericParameters.Count == 0)
				return; // non-generic

			os.Write("<");
			bool isfirst = true;
			foreach (GenericParameter p in methodDef.GenericParameters) {
				if (!isfirst)
					os.Write (",");
				os.Write (p.Name);
				isfirst = false;
			}
			os.Write (">");
		}

		private bool isExtensionMethod (MethodDefinition methodDef) 
		{
			foreach (CustomAttribute attr in methodDef.CustomAttributes)
				if (attr.Constructor.DeclaringType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute")
					return true;
			return false;
		}
		private void outputMethod (MethodDefinition methodDef, TextWriter os, bool isInteface, int indentation)
		{
			//TODO: Write method attributes
			os.WriteLine();

			if (SkipMethod (methodDef))
				return;
			
			if (methodDef.Name == "Finalize")
			{
				WriteToStream(os, "~" + methodDef.DeclaringType.Name + "() { throw new NotImplementedException(); }", indentation);
				return;
			}
			if (methodDef.IsPrivate && !methodDef.IsVirtual && !methodDef.IsConstructor && !methodDef.Name.Contains(".")) return;
			if (methodDef.IsInternalCall) return;
			if (methodDef.IsAssembly) return;
            
			if ((methodDef.SemanticsAttributes == MethodSemanticsAttributes.Getter) ||
			    (methodDef.SemanticsAttributes == MethodSemanticsAttributes.Setter) ||
			    (methodDef.SemanticsAttributes == MethodSemanticsAttributes.AddOn) ||
			    (methodDef.SemanticsAttributes == MethodSemanticsAttributes.RemoveOn))
				return;
			if (methodDef.IsCompilerControlled) return;
			if (methodDef.IsConstructor && methodDef.IsStatic) return;

			if ((methodDef.IsNewSlot || !methodDef.IsVirtual) && !methodDef.IsConstructor)
				writePerMethodAttributes (os, indentation);
			if (!isInteface)
			{				
				if (!methodDef.Name.Contains(".") || methodDef.IsConstructor) // no modifiers on explicit interface implementation
				{
					if (methodDef.IsPublic)
						WriteToStream (os, "public ", indentation);
					else if (methodDef.IsPrivate && !methodDef.IsConstructor)
						WriteToStream (os, "private ", indentation);
					else
						WriteToStream (os, "protected ", indentation);
                    

					//if (methodDef.IsAbstract) os.Write("abstract ");
					if (methodDef.IsStatic)
						os.Write ("static ");
					//if (methodDef.IsFinal) os.Write("final ");
					if (methodDef.IsVirtual) {
						if (methodDef.IsReuseSlot)
							WriteToStream (os, "override ");
						else if (!methodDef.IsFinal)
							os.Write ("virtual ");
					}
				}
			}
			if (!methodDef.IsConstructor)
				os.Write(getTypeName(methodDef.ReturnType.ReturnType) + " ");
			outputMethodName (methodDef, os);

			os.Write("(");
			bool isfirst = true;
			int pn = 0;
			foreach (ParameterDefinition param in methodDef.Parameters)
			{
				if (isfirst && isExtensionMethod (methodDef))
					os.Write ("this ");
				else if (!isfirst) os.Write(", ");
				isfirst = false;
				os.Write(getTypeName(param.ParameterType, param.IsOut) + "  " + "p" + pn);
				pn++;
			}
			if (isInteface) os.WriteLine(");");
			else{
				WriteToStream(os, ")\n");
				if (methodDef.IsConstructor)
					WriteCtor (methodDef, os, indentation);
				else
					WriteToStream(os,"{ throw new NotImplementedException(); }", indentation);
			}
		}		

		private bool AreSameSignature (MethodDefinition m1, MethodDefinition m2) {
			if (m1.Parameters.Count != m2.Parameters.Count)
				return false;
			if (m1.ReturnType.ReturnType.FullName != m2.ReturnType.ReturnType.FullName)
				return false;
			for (int i = 0; i < m1.Parameters.Count; ++i)
				if (m1.Parameters [i].ParameterType.FullName != m2.Parameters [i].ParameterType.FullName)
					return false;
			return true;
		}

		private void WriteCtor (IMemberReference methodDef, TextWriter os, int indentation)
		{
			TypeDefinition typeDef = _module.Types [methodDef.DeclaringType.FullName];

			if (typeDef.BaseType != null && typeDef.BaseType.FullName == "System.Attribute")
				WriteToStream(os, "{}", indentation);
			else 
				WriteToStream (os, "{ throw new NotImplementedException(); }", indentation);
		
		}

		private static void WriteToStream(TextWriter os, string text, int indentation)
		{
			for (int i = 0; i < indentation; ++i)
				os.Write("\t");
			os.Write (text);
		}	
		
		private static void WriteToStream(TextWriter os, string text)
		{
			WriteToStream(os, text, 0);
		}


		private static string [] _methodsToSkip = new string [] { "System.ComponentModel.ISite System.Windows.Forms.Control::get_Site()" };																  
		private static bool SkipMethod (MethodDefinition method) {

			if (Array.IndexOf (_methodsToSkip, method.ToString ()) >= 0)
				return true;
			OverrideCollection methods = method.Overrides;
			foreach (MethodReference m in methods) {
				if (getTypeName (m.DeclaringType) == IDONTIMPLEMENT)
					return true;
			}
			return false;
		}

		private static MethodDefinition GetPropertyMethod (PropertyDefinition prop) {
			if (prop.GetMethod != null)
				return prop.GetMethod;
			return prop.SetMethod;
		}

		private static bool IsVirtual (MethodDefinition m) {
			if (m.IsPrivate)
				return false;
			return m.IsVirtual && !m.IsReuseSlot && !m.IsFinal;
		}

		private static bool IsOverride (MethodDefinition m) {
			if (m.IsPrivate)
				return false;
			return m.IsVirtual && m.IsReuseSlot && !m.IsFinal;
		}

		private static bool IsStatic (MethodDefinition m) {
			return m.IsStatic;
		}

		private void OutputProperty(PropertyDefinition prop, TextWriter os, bool isInterface, int indentation)
		{
			os.WriteLine ();

			MethodDefinition getOrSet = GetPropertyMethod(prop);			

			if (SkipMethod (getOrSet))
				return;

			if (prop.GetMethod != null && !prop.GetMethod.IsPublic && !prop.GetMethod.IsFamily &&
				(!prop.GetMethod.IsVirtual || !prop.GetMethod.IsFinal))
				return;


			if (!isInterface)
			{
				if (GetPropertyMethod (prop) != null && GetPropertyMethod (prop).IsPublic) {
					WriteToStream (os, "public ", indentation);
				}
				else if (GetPropertyMethod (prop) != null && GetPropertyMethod (prop).IsFamily) {
					WriteToStream (os, "protected ", indentation);
				}
			}


			if (!isInterface) {
				if ((prop.GetMethod != null && IsVirtual( prop.GetMethod)) ||
				(prop.SetMethod != null && IsVirtual (prop.SetMethod ))){
					WriteToStream (os, "virtual ", indentation);
				}

				else if ((prop.GetMethod != null && IsOverride (prop.GetMethod)) ||
				(prop.SetMethod != null && IsOverride (prop.SetMethod))) {
					WriteToStream (os, "override ", indentation);
				}

				if ((prop.GetMethod != null && IsStatic (prop.GetMethod)) ||
				(prop.SetMethod != null && IsStatic (prop.SetMethod))) {
					WriteToStream (os, "static ", indentation);
				}
			}

			if ((prop.Name == "Item" || prop.Name.EndsWith(".Item")) && prop.GetMethod.Parameters.Count > 0)
			{
				string propname = prop.Name;
				propname = propname.Remove(propname.Length - 4) + "this";
				os.Write(getTypeName(prop.PropertyType) + "  " + propname + "[");
				int pn = 0;
				bool needscomma = false;
				foreach (ParameterDefinition typeDef in prop.GetMethod.Parameters)
				{
					if (needscomma) os.Write(", ");
					needscomma = true;
					os.Write(getTypeName(typeDef.ParameterType) + " p" + pn);
					pn++;
				}
				os.WriteLine("] {");
			} else
				os.WriteLine (getTypeName (prop.PropertyType) + "  " + prop.Name + "{");
			if (isInterface)
			{
				if (prop.GetMethod != null) {
					if (prop.GetMethod.IsNewSlot || !prop.GetMethod.IsVirtual)
						writePerMethodAttributes (os, indentation);
					WriteToStream (os, " get;", indentation + 1);
				}
				if (prop.SetMethod != null) {
					if (prop.SetMethod.IsNewSlot || !prop.SetMethod.IsVirtual)
						writePerMethodAttributes (os, indentation);
					WriteToStream (os, " set;", indentation + 1);
				}
			} else
			{
				if (prop.GetMethod != null) {
					if (prop.GetMethod.IsNewSlot || !prop.GetMethod.IsVirtual)
						writePerMethodAttributes (os, indentation);
					WriteToStream (os, " get { throw new NotImplementedException(); }\n", indentation + 1);
				}
				if (prop.SetMethod != null) {
					if ( prop.SetMethod.IsNewSlot || !prop.SetMethod.IsVirtual)
						writePerMethodAttributes (os, indentation);
					WriteToStream (os, " set { throw new NotImplementedException(); }\n", indentation + 1);
				}
			}
			WriteToStream (os, "}", indentation);
		}

		private static bool IsDelegate(TypeDefinition typeDef)
		{
			if (typeDef.BaseType != null && typeDef.BaseType.FullName == "System.MulticastDelegate") return true;
			return false;
		}

		private static void OutputDelegate(TypeDefinition typeDef, TextWriter os, int indentation)
		{
			WriteToStream (os, "public delegate ", indentation);
			foreach (MethodDefinition methodDef in typeDef.Methods)
				if (methodDef.Name == "Invoke")
				{
					os.Write(getTypeName(methodDef.ReturnType.ReturnType) + " ");
					os.Write(typeDef.Name);
					os.Write("(");
					bool isfirst = true;
					int pn = 0;
					foreach (ParameterDefinition param in methodDef.Parameters)
					{
						if (!isfirst) os.Write(", ");
						isfirst = false;
						os.Write(getTypeName(param.ParameterType) + "  " + "p" + pn);
						pn++;
					}
					os.WriteLine(");");
				}
		}

		private void OutputField (FieldDefinition f, StreamWriter os, int indentation) {
			System.Text.StringBuilder fieldStr = new System.Text.StringBuilder ();
			fieldStr.Append (GetModifiers (f));
			fieldStr.Append (getTypeName (f.FieldType) + " ");
			fieldStr.Append (f.Name + ";\n");
			WriteToStream (os, fieldStr.ToString(), indentation);
		}

		public string GetModifiers (FieldDefinition f) {
			string str = "";
			if (f.IsPublic)
				str += "public ";
			else
				str += "protected ";
			if (f.IsStatic)
				str += "static ";
			return str;
		}
		string getDeclaringTypeName(TypeReference typeDef)
		{
			if (typeDef.GenericParameters.Count == 0)  // not generic
				return typeDef.Name;
			string typename = typeDef.Name.Substring(0, typeDef.Name.IndexOf('`')) + "<";
			bool isfirst = true;
			foreach (GenericParameter p in typeDef.GenericParameters) {
				if (!isfirst) typename = typename + ",";
				typename = typename + p.Name;
				isfirst = false;
			}
			typename = typename + ">";
			return typename;
		}

		private void OutputType(TypeDefinition typeDef, StreamWriter os, int indention)
		{
			if (typeDef.IsNestedPrivate || typeDef.IsNestedAssembly || !typeDef.IsInterface && typeDef.IsNotPublic)
				return;

			/* need to output attributes
            foreach (CustomAttribute attribute in typeDef.CustomAttributes)
            {
                
            }
             */

			if (!typeDef.IsNested)
			{
				foreach (string s in _usingNamespaces)
					os.WriteLine("using " + s + ";");
				os.WriteLine("namespace " + typeDef.Namespace + " { ");
			}
			if (IsDelegate(typeDef))
			{
				OutputDelegate(typeDef, os, indention + 2);
				if (!typeDef.IsNested) os.WriteLine("}");
				return;
			}
			if (typeDef.IsInterface)
				//BUGBUG
				WriteToStream (os, "[vmw.common.ChangeInterfaceMethodNames]\n", indention + 1);
			if (!typeDef.IsNested)
			{
				if(typeDef.IsPublic)
					WriteToStream(os, "public ", indention + 1);
				else
					WriteToStream (os, "internal ", indention + 1);

			}else
			{
				if(typeDef.IsNestedPublic)
					WriteToStream (os, "public ", indention + 1);
				else 
					WriteToStream (os, "protected ", indention + 1);
			
			}
			if (typeDef.IsEnum)
			{
				os.WriteLine("enum " + typeDef.Name + " { ");
				foreach (FieldDefinition field in typeDef.Fields)
					if (field.Name != "value__")WriteToStream(os, field.Name + ",\n", indention + 2);
			}
			else if (typeDef.IsValueType)
			{
				os.WriteLine("struct " + typeDef.Name + " { ");
				// TODO implement members
			} else
			{
				if (typeDef.IsInterface) os.Write("interface ");
				else
				{
					if (typeDef.IsSealed && typeDef.IsAbstract)	os.Write ("static ");
					else if (typeDef.IsSealed) os.Write("sealed ");
					else if (typeDef.IsAbstract) os.Write("abstract ");
					if (typeDef.IsClass) os.Write("class ");
				}
				os.Write (getDeclaringTypeName (typeDef));
				bool needscomma = false;
				bool needscolon = true;
				if (typeDef.BaseType != null)
				{
					os.Write(" : "); 
					needscolon = false; 
					os.Write(getTypeName(typeDef.BaseType));
					needscomma = true;
				}
				foreach (TypeReference iface in typeDef.Interfaces)
				{
					if (needscolon) { os.Write(" : "); needscolon = false; }
					if (needscomma) os.Write(", ");
					needscomma = true;
					os.Write(getTypeName(iface));
				}
				os.WriteLine(" {");
				foreach (FieldDefinition f in typeDef.Fields) {
					if (f.IsPublic || f.IsFamily)
						OutputField (f, os, indention + 2);
				}
			}

			// TODO: Write nested classes recursivly
			foreach (TypeDefinition nested in typeDef.NestedTypes)
				OutputType (nested, os, indention + 1);

			bool hasDefaultConstructor = false;
			foreach (MethodDefinition cons in typeDef.Constructors)
			{
				outputMethod (cons, os, false, indention + 2);
				if (cons.Parameters.Count == 0 && !cons.IsStatic)
					hasDefaultConstructor = true;
			}
			if (!hasDefaultConstructor && typeDef.IsClass && !typeDef.IsValueType 
				&& !typeDef.IsSealed) {
				os.WriteLine ();

				// it is good to output default constructors, becaues otherwise inheriting classes will have a hard time
				// implementing a constructor that has to call a non-default constructor of the parent.
				//	os.WriteLine("public " + typeDef.Name + "() {  throw new NotImplementedException(); }");
				string typedefname = typeDef.Name;
				if (typedefname.IndexOf ("`") != -1)
					typedefname = typedefname.Substring (0, typedefname.IndexOf ("`"));
				WriteToStream(os, "internal " + typedefname + "()\n", indention + 2);
				WriteToStream (os, "{  throw new NotImplementedException(); }", indention + 2);
			}

			foreach (EventDefinition ev in typeDef.Events)
				OutputEvent(ev, os, typeDef.IsInterface, indention + 2);

			foreach (MethodDefinition method in typeDef.Methods)
				outputMethod(method, os, typeDef.IsInterface, indention + 2);

			foreach (PropertyDefinition prop in typeDef.Properties)
				OutputProperty(prop, os, typeDef.IsInterface, indention + 2);

			WriteToStream (os, "}\n", indention + 1);// close class
			if (!typeDef.IsNested) os.WriteLine("}"); // close namespace
		}

		private static void OutputEvent (EventDefinition ev, TextWriter os, bool isInterface, int indentation)
		{
			WriteToStream(os, "\n");
			if (ev.EventType.FullName == "System.Windows.Forms.DomainUpDown/CollectionChangedEventHandler")
				return;
			if (ev.EventType.FullName == "System.Windows.Forms.ListView/CollectionChangedHandler")
				return;

			if(!ev.Name.Contains(".") && !isInterface)
				WriteToStream (os, "public ", indentation);
			
			WriteToStream (os, "event " + ev.EventType.FullName, indentation);
			WriteToStream (os, " " + ev.Name, indentation);


			if (isInterface) {
				WriteToStream (os, ";\n");
				return;
			}

			WriteToStream(os, "{\n");
			if(ev.AddMethod != null)
				WriteToStream (os, "add " + EXCEPTION_IMPLEMENTATION, indentation + 1);
			if(ev.RemoveMethod != null)
				WriteToStream (os, "remove " + EXCEPTION_IMPLEMENTATION, indentation + 1);
			if(ev.InvokeMethod != null)
				WriteToStream (os, "invoke " + EXCEPTION_IMPLEMENTATION, indentation + 1);

			WriteToStream (os, "}", indentation);
		}

		private void OutputType(TypeDefinition typeDef)
		{
			if (typeDef.IsNestedPrivate || typeDef.IsNestedAssembly || !typeDef.IsInterface && typeDef.IsNotPublic)
				return;
			if ((typeDef.Namespace == null) || (typeDef.Namespace == ""))
				return;

			Directory.CreateDirectory(_basedir);
			Directory.CreateDirectory(_basedir + typeDef.Namespace);
			string fname =  typeDef.Namespace + '\\' + typeDef.Name.Replace('`','_') + ".cs";
			Console.WriteLine("Output type " + fname);
			string filePath = _basedir + fname;

			if (File.Exists (filePath))
				foreach (string s in _dontmodify)
					if (s == fname)
						return;

			StreamWriter os = new StreamWriter (filePath, false);
            
			OutputType(typeDef, os, 0);
			os.Close();
		}

		private void ConvertTypes ()
		{
			foreach (TypeDefinition typeDef in _module.Types)
				OutputType(typeDef);
		}
	}
}
